import ast
import logging
import pathlib
from typing import Dict, Optional, List, Tuple, Any, Set  # Added Set
import builtins

# Basic Logging
logger = logging.getLogger(__name__)

# --- Built-in Check ---
BUILTIN_NAMES = set(dir(builtins)) | {'typing'}

class Resolver:
    """
    Responsible for resolving names (variables, functions, classes, modules)
    to their fully qualified paths (full_path) within the project context.
    """
    current_method_context: Optional[Dict[str, Any]] = None

    def __init__(self, project_root: pathlib.Path, known_module_paths: Dict[str, pathlib.Path]):
        """
        Initializes the Resolver.

        Args:
            project_root: The absolute path to the root of the Python project.
            known_module_paths: A dictionary mapping known module full_paths
                                (e.g., 'my_package.utils') to their absolute file paths.
        """
        logger.debug(f"BUILTIN_NAMES initialized with {len(BUILTIN_NAMES)} items. Includes 'str': {'str' in BUILTIN_NAMES}, 'Exception': {'Exception' in BUILTIN_NAMES}")
        self.project_root: pathlib.Path = project_root.resolve()
        self.root_package_name: str = self.project_root.name
        logger.info(f"Resolver: Determined root package name: {self.root_package_name}")
        self.known_module_paths: Dict[str, pathlib.Path] = known_module_paths
        self.known_file_paths: Dict[pathlib.Path, str] = {v: k for k, v in known_module_paths.items()}
        self.current_module_full_path: Optional[str] = None
        self.current_file_path: Optional[pathlib.Path] = None
        self.current_module_import_map: Dict[str, str] = {}
        self.current_module_definitions: Dict[str, str] = {}

        logger.info(f"Resolver initialized for project root: {self.project_root}")
        logger.debug(f"Resolver knows {len(self.known_module_paths)} module paths. Sample: {list(self.known_module_paths.keys())[:10]}")

    def set_current_module(self, module_full_path: str, file_path: pathlib.Path) -> None:
        """Sets the context for the module currently being parsed."""
        logger.debug(f"Resolver context set to module: {module_full_path} ({file_path})")
        self.current_module_full_path = module_full_path
        self.current_file_path = file_path.resolve()
        self.current_module_import_map = {}
        self.current_module_definitions = {}

    def add_import(self, local_name: str, full_path: str) -> None:
        """Records a resolved import mapping for the current module."""
        # Avoid adding useless wildcard markers as valid imports
        if local_name == '*' and full_path.endswith(".*"):
            logger.debug(f"Skipping add_import for wildcard marker: '{local_name}' -> '{full_path}'")
            return
        logger.debug(f"Adding import to current module map: '{local_name}' -> '{full_path}'")
        self.current_module_import_map[local_name] = full_path

    def add_local_definition(self, name: str, full_path: str) -> None:
        """Records a name defined directly within the current module."""
        logger.debug(f"Adding local definition: '{name}' -> '{full_path}'")
        self.current_module_definitions[name] = full_path

    def _get_full_path_for_definition(self, name: str) -> Optional[str]:
        """Constructs the full_path for a name defined in the current module."""
        logger.debug(f"Resolver: Getting full path for definition '{name}' in module context '{self.current_module_full_path}'")
        if self.current_module_full_path is None:
            # Allow "" as a valid path (root __init__.py)
            logger.warning(f"Cannot get full path for definition '{name}', current module context not set.")
            return None

        if self.current_module_full_path == "":
            # Definitions in root __init__.py belong to the package namespace
            logger.debug(f"Root __init__.py definition '{name}', prepending root package name '{self.root_package_name}'.")
            # Ensure root_package_name is known
            if not self.root_package_name:
                logger.error("Cannot resolve root __init__ definition: root_package_name is not set in Resolver!")
                return None
            return f"{self.root_package_name}.{name}"

        # Standard case: module_path.name
        return f"{self.current_module_full_path}.{name}"

    def set_method_context(self, context: Optional[Dict[str, Any]]):
        """Called by Visitor when entering/leaving a method."""
        self.current_method_context = context

    def resolve(self, name: str, *, is_method_scope: bool = False) -> Optional[str]:
        """
        Attempts to resolve a simple name to its full_path in the current context.
        Order: Method Context -> Current Module Defs -> Imports -> Builtins -> Known Modules.
        """
        logger.debug(f"--> Resolve START: name='{name}', module='{self.current_module_full_path}', is_method='{is_method_scope}'")

        # --- Handle 'self'/'cls' in methods ---
        if is_method_scope and self.current_method_context:
            class_full_path = self.current_method_context.get('class_full_path')
            # Basic check for self
            if name == 'self' and class_full_path and not self.current_method_context.get('is_static'):
                logger.debug(f"Resolve: Resolved 'self' to class '{class_full_path}'")
                return class_full_path
            # Basic check for cls (less reliable without knowing decorator)
            if name == 'cls' and class_full_path and (self.current_method_context.get('is_classmethod') or self.current_method_context.get('is_metaclass_method')): # Assuming metaclass methods also use cls
                logger.debug(f"Resolve: Resolved 'cls' to class '{class_full_path}'")
                return class_full_path


        # 1. Check definitions within the current module
        logger.debug(f"Resolve: Checking current module definitions for '{name}'...")
        if name in self.current_module_definitions:
            resolved = self.current_module_definitions[name]
            logger.debug(f"Resolve: Found in current module definitions: '{resolved}'")
            return resolved

        # 2. Check imports in the current module
        logger.debug(f"Resolve: Checking imports for '{name}'...")
        if name in self.current_module_import_map:
            resolved = self.current_module_import_map[name]
            logger.debug(f"Resolve: Found in imports: '{resolved}'")
            # Handle potential placeholder from wildcard import resolution
            if resolved.endswith(".*"):
                logger.debug(f"Resolve: Import found but points to wildcard '{resolved}', cannot resolve specific name '{name}'.")
                return None
            return resolved

        # 3. Check for built-ins / common stdlib top-levels handled by BUILTIN_NAMES
        logger.debug(f"Resolve: Checking builtins/typing for '{name}'...")
        if name in BUILTIN_NAMES:
            resolved_path = f"builtins.{name}" if name != 'typing' else 'typing'
            logger.debug(f"Resolve: Found in builtins/typing: '{resolved_path}'")
            return resolved_path

        # 4. Check if the name itself is a known module found during discovery
        logger.debug(f"Resolve: Checking known modules for '{name}'...")
        if name in self.known_module_paths:
            logger.debug(f"Resolve: Found as known module: '{name}'")
            return name

        # TODO: 5. Check enclosing scopes (for nested functions/classes)

        logger.debug(f"--> Resolve FAILED for name '{name}' in module '{self.current_module_full_path}'")
        return None

    def resolve_attribute(self, base_node: ast.expr, attr_name: str, *, is_method_scope: bool = False) -> Optional[str]:
        """
        Attempts to resolve an attribute access (e.g., base.attr_name).
        """
        base_full_path: Optional[str] = None
        logger.debug(f"--> Resolve Attribute START: '.{attr_name}', base='{ast.unparse(base_node)}', is_method={is_method_scope}")

        if 'faker' in attr_name.lower() or (base_full_path and 'faker' in base_full_path.lower()):
            logger.debug(f"Resolve attribute base='{base_full_path}', attr='{attr_name}', returning: {resolved_attr_path_or_none}")        


        # Resolve the base part first
        if isinstance(base_node, ast.Name):
            base_full_path = self.resolve(base_node.id, is_method_scope=is_method_scope)
        elif isinstance(base_node, ast.Attribute):
            base_full_path = self.resolve_attribute(base_node.value, base_node.attr, is_method_scope=is_method_scope)
        # Add other base types if necessary (e.g., resolving result of function calls is hard)

        if base_full_path:
            # Simple approach: If base resolved, assume attribute exists under it.
            # This doesn't validate against actual class members yet.
            resolved_attr_path = f"{base_full_path}.{attr_name}"
            logger.debug(f"Resolve Attribute: Tentatively resolved attribute: '{resolved_attr_path}' from base '{base_full_path}'")
            return resolved_attr_path
        else:
            logger.debug(f"Could not resolve base '{ast.unparse(base_node)}' for attribute access '.{attr_name}'")
            return None

    def _resolve_absolute_import(self, module_parts: List[str]) -> Optional[str]:
        """Tries to find an absolute import path in known modules or assumes external."""
        potential_path = ".".join(module_parts)
        if not potential_path: # Should not happen for valid absolute imports
            logger.warning(f"Attempted to resolve an empty absolute import path.")
            return None

        # 1. Check direct match in known project modules
        if potential_path in self.known_module_paths:
            logger.debug(f"Absolute import '{potential_path}' found in known project modules.")
            return potential_path

        # 2. Check if it's a namespace package (prefix of a known module)
        is_namespace = False
        for known in self.known_module_paths:
            if known.startswith(potential_path + "."):
                is_namespace = True
                break
        if is_namespace:
            logger.debug(f"Absolute import '{potential_path}' resolved as known project namespace package.")
            return potential_path

        # 3. Assume external library or standard library module
        logger.debug(f"Absolute import path '{potential_path}' not found in project modules or namespaces. Assuming external/stdlib.")
        return potential_path

    def _resolve_relative_import(self, level: int, module_parts: Optional[List[str]], *, resolve_source_module: bool = False) -> Optional[str]:
        """Resolves relative imports based on the current module context."""
        if self.current_file_path is None:
            logger.warning("Cannot resolve relative import: current file path context not set.")
            return None

        # Use pathlib for robust path navigation
        current_dir = self.current_file_path.parent
        base_dir = current_dir
        for _ in range(level -1): # Go up level-1 times from current dir
            base_dir = base_dir.parent

        # Calculate the target directory/file path
        target_path: pathlib.Path
        if module_parts:
            target_path = base_dir.joinpath(*module_parts)
        else: # from . import X or from .. import X -> target is the base dir itself
            target_path = base_dir

        logger.debug(f"_resolve_relative_import: Calculated target disk path base: '{target_path}' from level {level}, parts {module_parts} in {self.current_file_path}")

        # Now, convert this target path back to a module path relative to project root
        target_module_path: Optional[str] = None
        try:
            relative_to_root = target_path.relative_to(self.project_root)
            # If the target path IS the project root, relative_to_root will be '.'
            if str(relative_to_root) == '.':
                target_module_path = "" # Root package
            else:
                target_module_path = ".".join(relative_to_root.parts)

            logger.debug(f"_resolve_relative_import: Calculated target module path base: '{target_module_path}'")

            # Check for potential file/directory ambiguity.
            # If module_parts were given, does target_path.py exist?
            # Or does target_path/__init__.py exist?
            potential_file = target_path.with_suffix(".py")
            potential_init = target_path / "__init__.py"

            # Prefer the __init__.py if both exist (it defines the package)
            if potential_init.is_file():
                # Module path is based on the directory path
                logger.debug(f"_resolve_relative_import: Target resolves to package dir '{target_path}' (using __init__.py)")
                # target_module_path already calculated based on directory is correct
            elif potential_file.is_file() and module_parts: # Check module_parts to avoid matching root/.py
                # Module path is based on the file path
                logger.debug(f"_resolve_relative_import: Target resolves to module file '{potential_file}'")
                # target_module_path already calculated based on directory needs '.py' stem added if different
                # This path math seems redundant if using known_module_paths lookup below
                pass # Let the known_module_paths check handle the final resolution
            else:
                # Neither file nor package found at target path
                logger.debug(f"_resolve_relative_import: No corresponding .py file or __init__.py found for target path '{target_path}'. Assuming namespace or external if possible.")
                # Keep target_module_path as calculated from directory for namespace check

        except ValueError:
            logger.warning(f"Relative import level {level} from {self.current_module_full_path} resolved path outside project root.")
            return None
        except Exception as e:
            logger.error(f"Error resolving relative import path math for {self.current_file_path}: {e}", exc_info=True)
            return None

        # We now have the candidate `target_module_path` (e.g., "providers.es")
        if target_module_path is None: # Should only happen on error above
            logger.error("_resolve_relative_import: target_module_path became None unexpectedly.")
            return None

        # +++ Add Debugging Check (copied from thought process) +++
        log_lookup_key = target_module_path
        # Use a sample for logging if needed, but check specific key if failing
        if log_lookup_key == 'providers.es' or log_lookup_key == 'providers.es_ES': # Check both problematic keys
            logger.debug(f"Checking for key '{log_lookup_key}' in known_module_paths. Keys sample: {list(self.known_module_paths.keys())[:20]}")
            if log_lookup_key not in self.known_module_paths:
                logger.error(f"CONFIRMED MISSING: Key '{log_lookup_key}' is NOT in known_module_paths at lookup time!")
            else:
                logger.info(f"CONFIRMED PRESENT: Key '{log_lookup_key}' IS in known_module_paths at lookup time!")
                # If present, why does 'in' fail? Check types?
                # logger.debug(f"Type of key: {type(log_lookup_key)}, Type of dict keys: {type(next(iter(self.known_module_paths.keys())))}")
        # --- End Debugging Check ---

        # Check 1: Direct match in known project modules
        if target_module_path in self.known_module_paths:
            logger.debug(f"_resolve_relative_import: SUCCESS (Direct Match): '{target_module_path}' found.")
            return target_module_path

        # Check 2: Namespace package match
        is_namespace = False
        for known in self.known_module_paths:
            if known.startswith(target_module_path + "."):
                is_namespace = True
                break
        if is_namespace:
            logger.debug(f"_resolve_relative_import: SUCCESS (Namespace Match): '{target_module_path}' found as namespace.")
            return target_module_path

        # Check 3: Special case for "from . import name" in top-level modules
        is_top_level_module = self.current_module_full_path is not None and '.' not in self.current_module_full_path and self.current_file_path.name != "__init__.py"
        if level == 1 and is_top_level_module and not module_parts: # e.g. from . import config in cli.py
            # This indicates importing from the root package itself.
            # The target_module_path would be "" (root). Check if root __init__ exists.
            if "" in self.known_module_paths:
                logger.debug(f"_resolve_relative_import: SUCCESS (Top-level 'from . import X'): Resolved to root package ''.")
                return "" # Return root package context
            else:
                logger.warning(f"_resolve_relative_import: Top-level 'from . import X' failed, root __init__.py ('') not found in known paths.")
                # Fall through to final failure


        logger.warning(f"_resolve_relative_import: FAILED (No Match): Target module path '{target_module_path}' (derived from level {level}, parts {module_parts} in {self.current_module_full_path}) not found in known modules or as namespace.")
        return None


    def resolve_import_statement(self, node: ast.Import) -> List[Tuple[str, str, str]]:
        """Resolves `import name [as alias], ...`"""
        resolved = []
        for alias in node.names:
            module_name = alias.name
            local_name = alias.asname or module_name
            # Resolve using absolute logic
            full_path = self._resolve_absolute_import(module_name.split('.'))
            if full_path:
                # Store source module path as well (it's the same for direct import)
                resolved.append((local_name, full_path, full_path))
                self.add_import(local_name, full_path)
            else:
                logger.warning(f"Could not resolve absolute import: 'import {module_name}' in {self.current_module_full_path}")
        return resolved

    def resolve_from_import_statement(self, node: ast.ImportFrom) -> List[Tuple[str, str, str]]:
        """Resolves `from module import name [as alias], ...`"""
        resolved = []
        level = node.level
        module_name_parts = node.module.split('.') if node.module else None

        source_module_full_path: Optional[str] = None
        if level == 0: # Absolute import: from project.utils import ... OR from external_lib import ...
            if not module_name_parts:
                logger.warning(f"Invalid 'from import ...' (level 0, no module) in {self.current_module_full_path}")
                return []
            source_module_full_path = self._resolve_absolute_import(module_name_parts)
        else: # Relative import: from . import ..., from ..utils import ...
            # module_name_parts can be None for "from . import name"
            source_module_full_path = self._resolve_relative_import(level, module_name_parts, resolve_source_module=True) # Resolve the module *being imported from*


        if source_module_full_path is None:
            # Log the failure source more clearly
            import_str = f"from {'.' * level}{node.module or ''} import ..."
            logger.warning(f"Could not resolve source module for '{import_str}' in {self.current_module_full_path}")
            return [] # Cannot resolve names if source module is unknown

        # Handle the imported names
        if node.names[0].name == '*':
            # Wildcard import. Mark the source module as used, but we can't resolve specific names.
            logger.warning(f"Encountered wildcard import 'from {source_module_full_path} import *' in {self.current_module_full_path}. Specific names not resolved.")
            # Add a special entry? Maybe just return source info.
            resolved.append(("*", f"{source_module_full_path}.*", source_module_full_path))
            # Do NOT add to self.current_module_import_map as it poisons lookups
            return resolved

        for alias in node.names:
            imported_name = alias.name
            local_name = alias.asname or imported_name
            # Assume the imported name exists within the source module's namespace.
            # We don't try to validate this during Pass 1 resolution.
            resolved_full_path = f"{source_module_full_path}.{imported_name}" if source_module_full_path else imported_name

            # Check if source_module_full_path resolved to the root package ""
            if source_module_full_path == "":
                resolved_full_path = imported_name # names in root are just their own name

            logger.debug(f"resolve_from_import_statement: Adding import map entry for module '{self.current_module_full_path}': '{local_name}' -> '{resolved_full_path}' (Source: {source_module_full_path})")
            resolved.append((local_name, resolved_full_path, source_module_full_path))
            self.add_import(local_name, resolved_full_path) # Add mapping for local use

        return resolved